#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>

// https://mp.weixin.qq.com/s/HdM8XBCkZgj61_ZbFGuCAQ
// LT 模式下，当发送数据完成后(是否完成是程序自己的逻辑)，依然会通知可写事件（因为这时候 socket 是可写的，系统并不知道你需要发送多少数据）
// LT 模式下正确的用法是，上来就发送数据，如果出现 EAGAIN 了，就使用 EPOLL 来监听，直到 epollout 事件发生继续些数据，一直写完
// ET 模式下，当发送完数据后，并不会继续通知 epollout 事件

#define min(a,b) ((a) <= (b) ? (a) : (b))

static int createAndBind(int port)
{
    int fd = socket(PF_INET, SOCK_STREAM, 0);

    int val = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));

    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(port);

    bind(fd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr_in));
    return fd;
}

static int makeNonBlocking(int sfd)
{
    int flags = fcntl(sfd, F_GETFL, 0);
    flags |= O_NONBLOCK;
    fcntl(sfd, F_SETFL, flags);

    return 0;
}

int main(void)
{
    bool useEt = true;
    int hadWrite = 0;
    int lastWrite = 0;
    int totalSize = 200 * 1024; // 假设最多发的数据

    char writeBuffer[1024];
    int bufferSize = sizeof(writeBuffer) / sizeof(char);
    for (int i = 0; i < bufferSize; i++)
    {
        writeBuffer[i] = i % 10 + '0';
    }

    int listenFd = createAndBind(9999);
    makeNonBlocking(listenFd);
    listen(listenFd, SOMAXCONN);

    // Since Linux 2.6.8, the size argument is ignored, but must be greater than zero;
    int epollFd = epoll_create(1);

    struct epoll_event ev;
    ev.data.fd = listenFd;
    ev.events = EPOLLIN;      // 这里不用 ET
    if (epoll_ctl(epollFd, EPOLL_CTL_ADD, ev.data.fd, &ev) == -1)
        perror("epoll ctl return -1");

    const int eventNumber = 10;
    struct epoll_event events[eventNumber];
    while (true)
    {
        int ret = epoll_wait(epollFd, events, eventNumber, -1);
        printf("epoll ret is %d\n", ret);
        if (ret <= 0)
        {
            // 为了简单忽略其他问题
            continue;
        }

        for (int i = 0; i < ret; i++)
        {
            int e = events[i].events;
            int fd = events[i].data.fd;
            if (fd == listenFd)
            {
                // 将新的链接加入到 epoll 中
                struct sockaddr_in cltAddr;
                socklen_t addrLen = sizeof(struct sockaddr_storage);
                int newFd = accept(listenFd, (struct sockaddr*)&cltAddr, &addrLen);
                makeNonBlocking(newFd);
                printf("accept new connection, fd is %d\n", newFd);

                struct epoll_event ev;
                ev.data.fd = newFd;
                ev.events = EPOLLOUT;
                if (useEt) ev.events |= EPOLLET;

                int ret = epoll_ctl(epollFd, EPOLL_CTL_ADD, ev.data.fd, &ev);
                if (ret == -1)
                {
                    printf("epoll_ctl return %d\n", ret);
                }
            }

            if (e & EPOLLOUT)
            {
                // 当写完数据后需要从 epoll 中移除，否则会一直触发 EPOLLOUT 事件
                while (hadWrite < totalSize)
                {
                    int size = write(fd, writeBuffer, min(bufferSize, totalSize - hadWrite));
                    if (size > 0)
                    {
                        hadWrite += size;
                        
                        // 使用 ET 模式的时候，如果不能一直写到 EAGAIN，在这里 break 的话，下次没有 EPOLLOUT 事件
                        if (useEt) errno = 0;
                    }
                    else
                    {
                        printf("write error, return: %d errno is %d\n", size, errno);
                        break;
                    }
                    // sleep(1);
                }

                printf("write +%d, %d - %d, errno is %d\n", hadWrite - lastWrite, lastWrite, hadWrite, errno);
                lastWrite = hadWrite;

                while (useEt && hadWrite >= totalSize && errno != 11)
                {
                    // 实验：对于 ET 模式，这里强制写，一直写到 EAGAIN 为止，
                    // 结论：对于 ET 模式，如果上一次 write 出现 EAGAIN 则下一次就会通知 epollout 事件
                    // 结论：对于 ET 模式，如果上一次 write 没有出现 EAGAIN 则下一次就不在通知
                    int size = write(fd, writeBuffer, bufferSize);
                    if (size > 0)
                    {
                        hadWrite += size;
                        printf("write more +%d, %d - %d, errno is %d\n", hadWrite - lastWrite, lastWrite, hadWrite, errno);
                        lastWrite = hadWrite;
                    }
                    else
                    {
                        printf("write more error, return: %d errno is %d\n", size, errno);
                    }
                }
            }

            if (e & (EPOLLHUP | EPOLLERR))
            {
                // if all fds are cloesd, should break
                printf("will close fd(%d), event is %d\n", fd, e);
                close(fd);
            }
        }
    }

    return 0;
}
